"""
定制类 介绍

形如__xxx__的变量或者函数名就要注意，这些在Python中是有特殊用途的。例如通过 __slots__ 可以限制实例的属性，__len__()方法是为了能让
class作用于len()函数。除此之外，Python的class中还有许多这样有特殊用途的函数，可以帮助我们定制类。
"""

# 1. __slots__()。正常情况下，当定义了一个 class,创建了一个 class 的实例后，可以给该实例绑定任何属性和方法,而通过 __slots__ 则可以
# 限制实例的属性


class Student(object):

    # 用元祖定义允许绑定的属性名称。注意：如果只有一个属性，可以省略括号
    __slots__ = ('name')


# 创建新的实例
s = Student()
# 绑定属性 'name'
s.name = 'Michael'
# 由于 score 没有被放到__slots__中，所以不能绑定 score 属性，试图绑定score将报：AttributeError: 'Student' object has no attribute 'score'
# s.score = 99

# 1.1 _slots__定义的属性仅对当前类实例起作用，对继承的子类是不起作用


class GraduateStudent(Student):
    pass


student = GraduateStudent()
print(dir(student))
student.name = 'Steven'
print(dir(student))
# 子类可以绑定新的属性。通过 dir 可见 age 属性已经绑定
student.age = 55
print(dir(student))

# 2. __str__()。默认通过 print() 打印出来的实例信息是所属类等信息。可以通过定义__str__()方法实现类似于Java里的toString()方法
print(student)


class Teacher:

    def __init__(self, name):
        self.name = name

    def __str__(self):
        return 'Student object (name: %s)' % self.name


teacher = Teacher('实现了 __str__')
# 通过打印可知，和上面打印的student不同。这样打印出来的实例不但好看，而且容易看出实例内部重要的数据。
print(teacher)

# 2.1 如果直接敲变量不用print，打印出来的实例还是好看。这是因为直接显示变量调用的不是__str__()，而是__repr__()，两者的区别是__str__()
# 返回用户看到的字符串，而__repr__()返回程序开发者看到的字符串，也就是说，__repr__()是为调试服务的。解决办法是再定义一个__repr__()。
# 但是通常__str__()和__repr__()代码都是一样的，所以可以直接将实现的 __str__() 赋值给 __repr__()


class Book:

    def __init__(self, name):
        self.name = name

    def __str__(self):
        return 'Student object (name: %s)' % self.name

    # 直接将 __str__ 赋给 __repr__
    __repr__ = __str__

# 3. __iter__()。如果一个类想被用于for ... in循环，类似list或tuple那样，就必须实现一个__iter__()方法，该方法返回一个迭代对象，然后，Python
# 的for循环就会不断调用该迭代对象的__next__()方法拿到循环的下一个值，直到遇到StopIteration错误时退出循环。以斐波那契数列为例，写一个Fib类，可以
# 作用于for循环


class Fib:

    def __init__(self, end):
        # 初始化两个计数器a，b
        self.a = 0
        self.b = 1
        # 退出循环的条件
        self.end = end

    def __iter__(self):
        # 实例本身就是迭代对象，故返回自己
        return self

    def __next__(self):
        # 计算下一个值
        self.a, self.b = self.b, self.a + self.b
        # 退出循环的条件
        if self.a > self.end:
            raise StopIteration()
        # 返回下一个值
        return self.a


n = 10
for i in Fib(n):
    print(i)

# 4. __getitem__()。如果一个类像要像list那样按照下标取出元素或实现切片操作，需要实现__getitem__()方法


class MySlice:

    def __init__(self, *s):
        self.slice = s

    def __getitem__(self, n):
        # 通过内置函数 isinstance 判断输入类型是否为整形或做切片操作
        a = isinstance(n, int)
        b = isinstance(n, slice)
        # 判断是通过下标获取元素值操作
        if a:
            # 判断是否越界
            if abs(n) >= len(self.slice):
                return '下标越界'
            # 输入负数处理
            if n < 0:
                return self.slice[len(self.slice) + n]
            return self.slice[n]
        # 判断是切片操作
        if b:
            start = n.start
            stop = n.stop
            step = n.step
            if start is None:
                start = 0
            if stop is None:
                stop = len(self.slice)
            if step is None:
                step = 1
            return list(self.slice[start:stop:step])
        # 如果不是整形或切片，则返回提示
        return '请输入整形或切片'


my_slice = MySlice(1, 2, 3, 4, 5, 6, 7, 8)
# 通过下标获取值
print(my_slice[1])
print(my_slice[0])
print(my_slice[-1])
print(my_slice[7])
# 非法的输入
print(my_slice['qqq'])
print(my_slice[8])
# 实现切片操作
my_slice_ = my_slice[:4]
print(my_slice_)
str_list = my_slice[1:]
print(str_list)
slice_ = my_slice[1:4]
print(slice_)
slice_1 = my_slice[2:6:2]
print(slice_1)

# 5. __ getattr__()。正常情况下，当我们调用类的方法或属性时，如果不存在，就会报错.要避免这个错误，除了可以动态加上这个属性外，Python
# 还有另一个机制，那就是写一个__getattr__()方法，动态返回一个属性。注意只有在没有找到属性的情况下，才调用__getattr__，已有的属性不会
# 在__getattr__中查找。对应的 __setattr__就是自定义 set 方法，不过一般不建议这么使用


class Stu:

    def __init__(self):
        self.name = 'Michael'

    # 当调用不存在的属性时，比如Python解释器会试图调用__getattr__(self, 'score')来尝试获得属性就会调用此方法
    def __getattr__(self, attr):
        print('__getattr__ is used')
        # 返回值
        if attr == 'score':
            return 99
        # 返回函数
        if attr == 'age':
            return lambda xx: xx + 10


stu = Stu()
# 获取初始化方法中定义的实例属性
print(stu.name)
# 调用不存在的属性 score ，此时解析器调用_getattr__(self, 'score')来尝试获得属性值。此属性我们设置了，所以会返回设定值
print(stu.score)
# 调用不存在的方法 age，此时解析器调用_getattr__(self, 'age')来尝试获取返回的函数表达式，并做计算，返回结果值
print(stu.age(20))
# sex 属性既不是实例属性，也没在 __getattr__() 中动态设置，而定义的__getattr__()默认返回就是None，所以返回值将士 None。当然如果要让
# class只响应特定的几个属性，可以按照约定显式在该方法末尾 raise AttributeError的错误，具体错误信息可自定义
print(stu.sex)

# 5.1 这种完全动态调用的特性有什么实际作用呢？作用就是可以针对完全动态的情况作调用。举个例子，现在很多网站都搞REST API，如果要写SDK，给
# 每个URL对应的API都写一个方法那得累死，而且API一旦改动SDK也要改。利用完全动态的__getattr__，可以写出一个链式调用。如下：


class Chain:

    def __init__(self, path=''):
        self.path = path

    # 根据传进来的path变量值调用不同的方法
    def __getattr__(self, path):
        # 也可以写成 self.path + '/' + path。这里使用了占位符来处理
        path_ = '%s/%s' % (self.path, path)
        return Chain(path_)


# 动态初始化
chain = Chain().status.user.timeline.list
print(chain.path)
# 还可以继续动态调用
dd = chain.dd
print(dd.path)

# 6. __ call__()。一个对象实例可以有自己的属性和方法，当我们调用实例方法时，我们用instance.method()来调用。能不能直接在实例本身上调用
# 呢？在Python中，答案是肯定的。任何类，只需要定义一个__call__()方法，就可以直接对实例进行调用。


class Stud:

    def __init__(self, name):
        self.name = name

    def __call__(self):
        print('%s 通过实例直接调用' % self.name)
        print('{0} 通过实例直接调用2'.format(self.name))


s = Stud('Michael')
# 对实例直接调用，就调用了 __call__() 方法
s()

# 6.1 __call__()还可以定义参数。对实例进行直接调用就好比对一个函数进行调用一样，所以完全可以把对象看成函数，把函数看成对象，因为这两者
# 之间本来就没啥根本的区别。如果你把对象看成函数，那么函数本身其实也可以在运行期动态创建出来，因为类的实例都是运行期创建出来的，这么一来，
# 我们就模糊了对象和函数的界限。那么，怎么判断一个变量是对象还是函数呢？我们需要判断一个对象是否能被调用，能被调用的对象就是一个Callable对象

print(callable(Stud))
print(callable(max))
print(callable(int))
print(callable(abs))
print(callable(str))
print(callable('str'))
print(callable(1))
print(callable([1, 3, 5, 7]))
